package uk.ac.bris.cs.scotlandyard.model;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import uk.ac.bris.cs.scotlandyard.model.Board.GameState;
import uk.ac.bris.cs.scotlandyard.model.Piece.Detective;
import uk.ac.bris.cs.scotlandyard.model.Piece.MrX;
import uk.ac.bris.cs.scotlandyard.model.ScotlandYard.Factory;

import javax.annotation.Nonnull;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * cw-model
 * Stage 1: Complete this class
 */
public final class MyGameStateFactory implements Factory<GameState> {
  @Nonnull
  @Override
  public GameState build(GameSetup setup, Player mrX, ImmutableList<Player> detectives) {
    return new MyGameState(setup, ImmutableSet.of(MrX.MRX), ImmutableList.of(), mrX, detectives);
  }

  private static final class MyGameState implements GameState {
    private final GameSetup setup;
    private ImmutableSet<Piece> remaining;
    private ImmutableList<LogEntry> log;
    private Player mrX;
    private List<Player> detectives;
    private ImmutableSet<Move> moves;
    private ImmutableSet<Piece> winner;
    private Boolean over;
    private Boolean detectiveWins;
    private final ImmutableList<Player> everyone;

    private MyGameState(final GameSetup setup,
                        final ImmutableSet<Piece> remaining,
                        final ImmutableList<LogEntry> log,
                        final Player mrX,
                        final List<Player> detectives) {
      this.setup = setup;
      this.remaining = remaining;
      this.log = log;
      this.mrX = mrX;
      this.detectives = detectives;
      var tmpEveryone = new ArrayList<Player>();
      tmpEveryone.add(mrX);
      tmpEveryone.addAll(detectives);
      this.everyone = ImmutableList.copyOf(tmpEveryone);
      this.over = false;
      this.detectiveWins = false;

      if (mrX == null) {
        throw new NullPointerException("mrX is null");
      }
      if (!mrX.isMrX()) {
        throw new IllegalArgumentException("No MrX");
      }

      for (int i = 0; i < detectives.size(); ++i) {
        if (detectives.get(i).isMrX()) {
          throw new IllegalArgumentException("more than one mrX");
        }

        if (detectives.get(i).tickets().get(ScotlandYard.Ticket.SECRET) > 0) {
          throw new IllegalArgumentException("detective have secret ticket");
        }
        if (detectives.get(i).tickets().get(ScotlandYard.Ticket.DOUBLE) > 0) {
          throw new IllegalArgumentException("detective have double ticket");
        }

        for (int t = i + 1; t < detectives.size(); ++t) {
          if (detectives.get(i).piece() == detectives.get(i + 1).piece()) {
            throw new IllegalArgumentException("duplicated detective");
          }
          if (detectives.get(i).location() == detectives.get(i + 1).location()) {
            throw new IllegalArgumentException("duplicated detectives' location");
          }
        }
      }
      if (setup.moves.isEmpty()) {
        throw new IllegalArgumentException("empty moves");
      }
      if (setup.graph.nodes().isEmpty()) {
        throw new IllegalArgumentException("Empty graph");
      }

      getAvailableMoves();

      var remainingList = new ArrayList<>(this.remaining);
      if (remainingList.get(0).isDetective()) {
        if (this.moves.isEmpty()) {
          this.remaining = ImmutableSet.of(mrX.piece());
          getAvailableMoves();
        }
      }
    }

    public ImmutableSet<Move> makeSingleMoves(GameSetup setup,
                                              List<Player> detectives,
                                              Player player,
                                              int source) {
      Move.SingleMove move;
      final ArrayList<Move> singleMoves = new ArrayList<>();

      for (int destination : setup.graph.adjacentNodes(source)) {
        boolean valid = true;
        for (var p : detectives) {
          if (p.location() == destination && p.isDetective()) {
            valid = false;
            break;
          }
        }
        if (!valid) continue;

        for (var t : Objects.requireNonNull(setup.graph.edgeValueOrDefault(source, destination, ImmutableSet.of()))) {
          if (player.has(t.requiredTicket())) {
            move = new Move.SingleMove(player.piece(), player.location(), t.requiredTicket(), destination);
            singleMoves.add(move);
            if (player.has(ScotlandYard.Ticket.DOUBLE)) {
              if (getSetup().moves.size() > 2) {
                singleMoves.addAll(makeDoubleMoves(detectives, player.use(t.requiredTicket()), destination, move));
              }
            }
          }
        }
        if (player.has(ScotlandYard.Ticket.SECRET)) {
          move = new Move.SingleMove(player.piece(), player.location(), ScotlandYard.Ticket.SECRET, destination);
          singleMoves.add(move);
          player.use(ScotlandYard.Ticket.SECRET);
          if (player.has(ScotlandYard.Ticket.DOUBLE)) {
            if (getSetup().moves.size() > 2) {
              singleMoves.addAll(makeDoubleMoves(detectives, player, destination, move));
            }
          }
        }
      }

      return ImmutableSet.copyOf(singleMoves);
    }

    public ImmutableSet<Move> makeDoubleMoves(
        List<Player> detectives,
        Player player,
        int source,
        Move move) {
      var doubleMoves = new HashSet<Move>();

      for (int destination : setup.graph.adjacentNodes(source)) {
        boolean valid = true;
        for (var p : detectives) {
          if (p.location() == destination && p.isDetective()) {
            valid = false;
            break;
          }
        }
        if (!valid) continue;

        for (var t : Objects.requireNonNull(setup.graph.edgeValueOrDefault(source, destination, ImmutableSet.of()))) {
          if (player.has(t.requiredTicket())) {
            Move.DoubleMove moveTogether = new Move.DoubleMove(player.piece(), move.source(), move.tickets().iterator().next(), source, t.requiredTicket(), destination);
            doubleMoves.add(moveTogether);
          }
        }
        if (player.has(ScotlandYard.Ticket.SECRET)) {
          Move.DoubleMove moveTogether = new Move.DoubleMove(player.piece(), move.source(), move.tickets().iterator().next(), source, ScotlandYard.Ticket.SECRET, destination);
          doubleMoves.add(moveTogether);
        }
      }
      return ImmutableSet.copyOf(doubleMoves);
    }


    @Override
    @Nonnull
    public GameSetup getSetup() {
      return setup;
    }

    @Override
    @Nonnull
    public ImmutableSet<Piece> getPlayers() {
      var tmp = new HashSet<Piece>();
      tmp.add(mrX.piece());
      tmp.addAll(detectives.stream().map(Player::piece).toList());
      remaining = ImmutableSet.copyOf(tmp);
      return remaining;
    }

    private int getLoc(@Nonnull Move move) {
      return move.accept(new Move.Visitor<>() {
        @Override
        public Integer visit(Move.SingleMove move) {
          return move.destination;
        }

        @Override
        public Integer visit(Move.DoubleMove move) {
          return move.destination2;
        }
      });
    }

    @Override
    @Nonnull
    public GameState advance(Move move) {
      moves = getAvailableMoves();
      if (!moves.contains(move)) throw new IllegalArgumentException("Illegal move: " + move);

      var remainingList = new ArrayList<>(remaining);
      var tmpLog = new ArrayList<>(log);

      var tmpRemaining = new HashSet<Piece>();
      final Player[] currentPlayer = new Player[1];
      final Player[] tmpMrX = {mrX};
      AtomicInteger index = new AtomicInteger(0);
      int indexLog = log.size();

      everyone.forEach(player -> {
        if (player.piece() == move.commencedBy())
          index.set(everyone.indexOf(player));
      });
      currentPlayer[0] = everyone.get(index.get());
      if (move.commencedBy().isMrX() && currentPlayer[0].isMrX()) {
        int loc = getLoc(move);
        currentPlayer[0] = currentPlayer[0].at(loc);

        tmpLog.addAll(move.accept(new Move.Visitor<>() {
          @Override
          public List<LogEntry> visit(Move.SingleMove move) {
            var lg = new ArrayList<LogEntry>();
            if (!setup.moves.get(indexLog)) {
              lg.add(LogEntry.hidden(move.ticket));
            }
            if (setup.moves.get(indexLog)) {
              lg.add(LogEntry.reveal(move.ticket, move.destination));
            }

            return lg;
          }

          @Override
          public List<LogEntry> visit(Move.DoubleMove move) {
            List<LogEntry> lg = new ArrayList<>();
            if (!setup.moves.get(indexLog)) {
              lg.add(LogEntry.hidden(move.ticket1));
              if (!setup.moves.get(indexLog + 1)) {
                lg.add(LogEntry.hidden(move.ticket2));
              }
              if (setup.moves.get(indexLog + 1)) {
                lg.add(LogEntry.reveal(move.ticket2, move.destination2));
              }
            }
            if (setup.moves.get(indexLog)) {
              lg.add(LogEntry.reveal(move.ticket1, move.destination1));
              if (!setup.moves.get(indexLog + 1)) {
                lg.add(LogEntry.hidden(move.ticket2));
              }
              if (setup.moves.get(indexLog + 1)) {
                lg.add(LogEntry.reveal(move.ticket2, move.destination2));
              }
            }
            return lg;
          }
        }));

        log = ImmutableList.copyOf(tmpLog);

        Iterable<ScotlandYard.Ticket> ticketIterator = move.accept(new Move.Visitor<>() {
          @Override
          public Iterable<ScotlandYard.Ticket> visit(Move.SingleMove move) {
            return move.tickets();
          }

          @Override
          public Iterable<ScotlandYard.Ticket> visit(Move.DoubleMove move) {
            return move.tickets();
          }
        });

        ticketIterator.forEach(ticket -> currentPlayer[0] = currentPlayer[0].use(ticket));

        mrX = currentPlayer[0];
        tmpRemaining.addAll(new ArrayList<>(detectives).stream().map(Player::piece).toList());
        remaining = ImmutableSet.copyOf(tmpRemaining);
      }

      if (move.commencedBy().isDetective() && currentPlayer[0].isDetective()) {
        int loc = getLoc(move);
        currentPlayer[0] = currentPlayer[0].at(loc);
        Iterable<ScotlandYard.Ticket> ticketIterator;
        ticketIterator = move.accept(new Move.Visitor<>() {
          @Override
          public Iterable<ScotlandYard.Ticket> visit(Move.SingleMove move) {
            return move.tickets();
          }

          @Override
          public Iterable<ScotlandYard.Ticket> visit(Move.DoubleMove move) {
            return move.tickets();
          }
        });

        ticketIterator.forEach(ticket -> {
          currentPlayer[0] = currentPlayer[0].use(ticket);
          tmpMrX[0] = tmpMrX[0].give(ticket);
        });

        var tmpDet = new ArrayList<Player>();
        mrX = tmpMrX[0];
        for (var d : detectives) {
          if (d.piece() == currentPlayer[0].piece()) {
            int n = detectives.indexOf(d);
            for (int idx = 0; idx < n; idx++) {
              tmpDet.add(detectives.get(idx));
            }
            tmpDet.add(currentPlayer[0]);
            for (int idx = n + 1; idx < detectives.size(); idx++) {
              tmpDet.add(detectives.get(idx));
            }
          }
        }
        detectives = tmpDet;

        if (remainingList.get(0).isMrX()) {
          remainingList.clear();
          remainingList.addAll(detectives.stream().map(Player::piece).toList());
        }
        if (remainingList.get(0).isDetective() && remainingList.size() == 1) {
          remainingList.remove(0);
          remainingList.add(mrX.piece());
        }
        if (remainingList.get(0).isDetective() && remainingList.size() > 1) {
          remainingList.remove(currentPlayer[0].piece());
        }
        tmpRemaining.addAll(remainingList);

        remaining = ImmutableSet.copyOf(tmpRemaining);
      }

      return new MyGameState(setup, remaining, log, mrX, detectives);
    }

    @Override
    @Nonnull
    public Optional<Integer> getDetectiveLocation(Detective detective) {
      AtomicInteger j = new AtomicInteger(0);
      detectives.forEach(player -> {
        if (detective == player.piece())
          j.set(player.location());
      });
      if (j.get() != 0) {
        return Optional.of(j.get());
      } else return Optional.empty();
    }

    @Override
    @Nonnull
    public Optional<TicketBoard> getPlayerTickets(Piece piece) {
      for (var p : everyone) {
        if (p.piece() == piece) {
          return Optional.of(ticket -> p.tickets().getOrDefault(ticket, 0));
        }
      }
      return Optional.empty();
    }

    @Override
    @Nonnull
    public ImmutableList<LogEntry> getMrXTravelLog() {
      return log;
    }

    public ImmutableSet<Piece> setWinner() {
      if (!over) {
        winner = ImmutableSet.of();
      }
      if (over && detectiveWins) {
        winner = ImmutableSet.copyOf(new HashSet<Piece>(detectives.stream().map(Player::piece).toList()));
      }
      if (over && !detectiveWins) {
        winner = ImmutableSet.of(mrX.piece());
      }
      return winner;
    }

    @Override
    @Nonnull
    public ImmutableSet<Piece> getWinner() {
      if (over) {
        setWinner();
      }
      if (!over) {
        var tixDetEmpty = new ArrayList<Boolean>();
        var remainingList = new ArrayList<>(remaining);
        for (var p : detectives) {
          boolean tixEmpty = false;
          if (p.location() == mrX.location()) {
            over = true;
            detectiveWins = true;
            break;
          }

          for (var v : p.tickets().values()) {
            if (!v.equals(0)) {
              tixEmpty = false;
              break;
            } else tixEmpty = true;
          }

          if (tixEmpty) {
            tixDetEmpty.add(true);
          } else tixDetEmpty.add(false);
        }

        if (over) {
          return setWinner();
        }

        for (var b : tixDetEmpty) {
          if (!b) {
            over = false;
            break;
          } else {
            over = true;
            detectiveWins = false;
          }
        }

        if (over) {
          return setWinner();
        }

        if (moves.isEmpty() && remainingList.get(0).isMrX()) {
          over = true;
          detectiveWins = true;
        }

        if (over) {
          return setWinner();
        }

        if (log.size() >= setup.moves.size() && remaining.contains(mrX.piece())) {
          over = true;
          detectiveWins = false;
        }

        if (over) {
          return setWinner();
        }

        setWinner();
      }
      return winner;
    }

    @Override
    @Nonnull
    public ImmutableSet<Move> getAvailableMoves() {
      var tmpMove = new HashSet<Move>();
      var tmpR = new ArrayList<>(remaining);

      if (tmpR.get(0).isMrX()) {
        tmpMove.addAll(makeSingleMoves(setup, detectives, mrX, mrX.location()));
        moves = ImmutableSet.copyOf(tmpMove);
      }
      if (tmpR.get(0).isDetective()) {
        for (var piece : tmpR) {
          for (var p : detectives) {
            if (piece == p.piece()) {
              tmpMove.addAll(makeSingleMoves(setup, detectives, p, p.location()));
            }
          }
        }
        moves = ImmutableSet.copyOf(tmpMove);
      }

      if (!getWinner().isEmpty()) moves = ImmutableSet.of();

      return moves;
    }
  }
}
